feat(bindx-uploader): let consumers retarget an upload before it writes - #106
Closed
matej21 wants to merge 2 commits into
Closed
feat(bindx-uploader): let consumers retarget an upload before it writes#106matej21 wants to merge 2 commits into
matej21 wants to merge 2 commits into
Conversation
Uploader and MultiUploader hard-coded every UploaderEvents handler to a noop, so there was no supported way to intervene between "files dropped" and "the uploader writes into the target entity". Both now accept the event callbacks as optional props and compose them with the internal accept-type resolution, disconnect and extractor fill. Uploader also gains prepareTarget: it runs once per batch, before the target is disconnected or filled, and the target it returns is what the upload lands on. This unblocks copy-on-write flows where a shared, already-persisted row must be forked first instead of mutated in place. Fixes #63 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Euwkf2wtutqvtE4YRutU5R
…idation prepareTarget ran before the accept check, so dropping an unacceptable file into an uploader forked the target and then rejected every file: the relation ended up pointing at a fresh empty entity instead of the shared one it had. Losing a reference by dropping the wrong file is a worse outcome than the redundant work it was meant to avoid. It now runs after the prepare/filter phase, receives the accepted files, and is skipped when none passed. A throw still reports the batch through onError and abandons it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Euwkf2wtutqvtE4YRutU5R
Member
Author
|
Consolidated into #109, merged there. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The missing seam
Uploaderdeclared a fullUploaderEventscontract but exposed none of it: the component hard-coded every handler to a noop, soUploaderPropswas only{ entity, fileType, children }. There was no supported way to intervene between "user dropped files" and "the uploader starts writing into the target entity", and no way to change which entity the upload lands on.MultiUploaderPropshad the same gap.Consumers were left overriding
UploaderUploadFilesContextbetweenUploaderand the dropzone, queueing the dropped files, forking the relation, remounting the uploader with a newkeyand replaying the queue — which leans on remount timing and on a private context staying stable.The API
Two parts.
1. The event props are forwarded.
UploaderandMultiUploaderaccept theUploaderEventscallbacks as optional props and pass them to the internal hooks. A user handler composes with the built-in behaviour rather than replacing it: accept-type resolution, the has-one disconnect and the extractor fill all still run. OnlyonErrorhas a default (uploaderErrorHandler), which a supplied handler replaces.2.
UploadergainsprepareTarget.It runs once per batch, after the files pass validation (the accept check and any user
onBeforeUpload) and before the target is touched — in particular before$disconnect(). It receives the accepted files, and is skipped entirely when none passed, so a batch that is rejected outright never forks anything. The target it returns is the one that gets disconnected and filled; returning nothing keeps theentityprop's target, so existing usage is unchanged. A throw reports the batch throughonErrorand abandons it.What it unblocks
Copy-on-write. In a CMS media library a block references a shared, already-persisted asset; uploading a replacement must not mutate the shared row. The block can now fork to a fresh asset inside
prepareTargetand return the fresh relation, and the upload lands there — no remount, no private context:Re-reading the relation inside
prepareTargetis what makes this work.HasOneHandle.entitydoes follow its own target when the related id changes, but the capturedblock.asset.imagehangs off the old asset'sEntityHandle, so forkingblock.assetleaves that chain pointing at the shared row.Implementation
useUploaderDoUploadgained one batch-level step,onPrepareUpload(files), run between the prepare/filter phase and the upload loop. The prepare phase never writes to the entity — the first write is the$disconnect()inonStartUpload— so this is still ahead of everything that touches the target, while an unacceptable file can no longer trigger a fork.useFillEntityowns the resolved target: it returnsprepareUploadalongside the events and keeps the prepared target in a ref that the disconnect and the fill both read. The ref is reset on every batch.useFillEntity,useUploadState,useUploaderDoUpload) now takePartial<UploaderEvents>, matching the optional-chained calls those hooks already made. The publicUploaderEventstype is unchanged.MultiUploaderdoes not takeprepareTarget— every file already gets its own new item in the has-many, so there is nothing to fork.Tests
packages/bindx-uploader/tests/uploader.test.tsx— the first end-to-end tests of these components (existing uploader tests only exercised the contexts). Eight tests over a small media-library schema onMockAdapter:onBeforeUploadruns and can reject a file, withonErrorreceiving the rejection;prepareTargetretargets the upload to the forked asset, while a second subscription proves the shared asset keeps its image and its URL;prepareTarget, then the write;prepareTargetand leaves the target connected and unmodified;MultiUploaderforwards user handlers while still creating and filling its item.All of them fail on
main, and the two ordering-sensitive ones also fail against the first version of this branch.bun run typecheckclean;bun run test2034 pass / 0 fail; eslint clean.Fixes #63
🤖 Generated with Claude Code
https://claude.ai/code/session_01Euwkf2wtutqvtE4YRutU5R